/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { ts, ArkFile, AstTreeUtils } from 'arkanalyzer';
import { Defects, FileMatcher, MatcherCallback, MatcherTypes, Rule } from '../../Index';
import path from 'path';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { IssueReport } from '../../model/Defects';

enum Usefulness {
    Always = 'always',
    Never = 'will',
    Sometimes = 'may',
}
interface Options {
    ignoredTypeNames?: string[];
}

interface VariableInfo {
    hasObject: boolean;
    hasPrimitive: boolean;
    isMaybeObject: boolean;
    isDefinitelyObject: boolean;
    isSafelyConverted: boolean;  // 标记变量是否已经安全转换
}

export class NoBaseToStringCheck implements BaseChecker {
    private reportedNodes = new Map<string, Usefulness>();
    private sourceMap = new Map<string, string[]>();
    private safeVariables: Set<string> = new Set();
    private readonly LITERAL_TO_STRING_REGEX = /(?:\(\s*\{\s*\}\s*\)|\{\s*\})\s*\.toString\(/;
    readonly metaData: BaseMetaData = {
        severity: 2,
        ruleDocPath: 'docs/no-base-to-string-check.md',
        description: 'Require .toString() to only be called on objects which provide useful information when stringified.',
    };
    private dangerousVariables: Set<string> = new Set();
    private maybeObjectVariables: Map<string, VariableInfo> = new Map();
    private conditionalBlockVariables: Map<string, Set<number>> = new Map();
    public rule: Rule;
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private options: Options = {
        ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams', 'object', 'Object']
    } as Required<Options>;

    constructor() {
        this.dangerousVariables = new Set();
        this.maybeObjectVariables = new Map();
        this.conditionalBlockVariables = new Map();
    }

    registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check.bind(this)
        };
        return [matchFileCb];
    }

    public check(arkFile: ArkFile): void {
        this.options = this.rule && this.rule.option[0] ? this.rule.option as Options : this.options;
        try {
            this.dangerousVariables.clear();
            this.safeVariables.clear();
            this.maybeObjectVariables.clear();
            this.conditionalBlockVariables.clear();

            const filePath = arkFile.getFilePath();
            const code = arkFile.getCode();
            this.sourceMap.set(filePath, code.split('\n'));

            // 使用 AstTreeUtils 代替直接创建 SourceFile
            const sourceFile = AstTreeUtils.getASTNode(arkFile.getName(), code);

            // 第一轮：找出所有条件语句中安全转换的变量
            this.findSafeConversions(filePath, sourceFile);

            // 第二轮：检测可能的问题
            this.traverseNodes(filePath, sourceFile);
        } catch (err) {
            return;
        }
    }

    private findSafeConversions(filePath: string, node: ts.Node): void {
        // 找到所有 if (typeof X === 'object') { X = JSON.stringify(X); } 类型的安全转换
        if (ts.isIfStatement(node)) {
            this.checkIfStatementForSafeConversion(node);
        }

        // 递归检查子节点
        ts.forEachChild(node, child => {
            this.findSafeConversions(filePath, child);
        });
    }

    /**
     * 检查if语句是否包含安全转换
     */
    private checkIfStatementForSafeConversion(node: ts.IfStatement): void {
        // 确保if语句有条件表达式和语句体
        if (!node.expression || !node.thenStatement) {
            return;
        }

        // 检查条件是否为 typeof x === 'object'
        const condition = node.expression;
        if (!this.isTypeofObjectCondition(condition)) {
            return;
        }

        // 获取变量名
        const variableName = this.getVariableNameFromTypeofCondition(condition);
        if (!variableName) {
            return;
        }

        // 检查语句体
        this.checkThenStatementForSafeConversion(node.thenStatement, variableName, node);
    }

    /**
     * 检查条件是否为 typeof x === 'object'
     */
    private isTypeofObjectCondition(condition: ts.Expression): boolean {
        if (!ts.isBinaryExpression(condition)) {
            return false;
        }

        if (condition.operatorToken.kind !== ts.SyntaxKind.EqualsEqualsEqualsToken) {
            return false;
        }

        const left = condition.left;
        const right = condition.right;

        return ts.isTypeOfExpression(left) &&
            ts.isIdentifier(left.expression) &&
            ts.isStringLiteral(right) &&
            right.text === 'object';
    }

    /**
     * 从typeof条件中获取变量名
     */
    private getVariableNameFromTypeofCondition(condition: ts.Expression): string | null {
        if (!ts.isBinaryExpression(condition)) {
            return null;
        }

        const left = condition.left;
        if (!ts.isTypeOfExpression(left) || !ts.isIdentifier(left.expression)) {
            return null;
        }

        return left.expression.text;
    }

    /**
     * 检查if语句体中是否有安全转换
     */
    private checkThenStatementForSafeConversion(thenStatement: ts.Statement, variableName: string, ifNode: ts.IfStatement): void {
        // 只处理代码块
        if (!ts.isBlock(thenStatement)) {
            return;
        }

        // 遍历代码块中的语句
        for (const stmt of thenStatement.statements) {
            if (this.isJSONStringifyAssignment(stmt, variableName)) {
                this.markSafeConversion(variableName, ifNode);
                break;
            }
        }
    }

    /**
     * 检查语句是否为 x = JSON.stringify(x)
     */
    private isJSONStringifyAssignment(stmt: ts.Statement, variableName: string): boolean {
        if (!ts.isExpressionStatement(stmt)) {
            return false;
        }

        const expression = stmt.expression;
        if (!ts.isBinaryExpression(expression) ||
            expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
            return false;
        }

        const leftAssign = expression.left;
        const rightAssign = expression.right;

        if (!ts.isIdentifier(leftAssign) || leftAssign.text !== variableName) {
            return false;
        }

        if (!ts.isCallExpression(rightAssign) ||
            rightAssign.expression.getText() !== 'JSON.stringify') {
            return false;
        }

        return rightAssign.arguments.length === 1 &&
            rightAssign.arguments[0].getText() === variableName;
    }

    /**
     * 标记变量为安全转换
     */
    private markSafeConversion(variableName: string, ifNode: ts.IfStatement): void {
        // 更新变量信息
        const varInfo = this.maybeObjectVariables.get(variableName);
        if (varInfo) {
            varInfo.isSafelyConverted = true;
            this.maybeObjectVariables.set(variableName, varInfo);
        }

        // 记录变量在条件块中被安全转换的位置
        const startPos = ifNode.getStart();
        let posSet = this.conditionalBlockVariables.get(variableName) || new Set<number>();
        posSet.add(startPos);
        this.conditionalBlockVariables.set(variableName, posSet);

        // 也添加到安全变量集合中
        this.safeVariables.add(variableName);
    }

    private traverseNodes(filePath: string, node: ts.Node): void {
        try {
            this.checkNodeByType(filePath, node);
            // 继续遍历子节点
            this.traverseChildNodes(filePath, node);
        } catch (e) {
            return;
        }
    }

    private checkNodeByType(filePath: string, node: ts.Node): void {
        // 检查变量声明，特别是条件表达式初始化
        if (ts.isVariableDeclaration(node)) {
            this.checkVariableDeclaration(filePath, node);
            this.markVariableDeclaration(node);
            return;
        }

        // 检查赋值表达式，特别是条件表达式赋值
        if (ts.isBinaryExpression(node) &&
            node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
            ts.isIdentifier(node.left)) {
            this.checkAssignmentExpression(filePath, node);
            this.markAssignmentExpression(node);
            return;
        }

        // 检查直接的对象字面量调用 toString()
        if (ts.isCallExpression(node) &&
            ts.isPropertyAccessExpression(node.expression) &&
            node.expression.name.text === 'toString') {
            this.checkToStringCall(filePath, node);
            return;
        }

        // 检查模板字符串表达式
        if (ts.isTemplateExpression(node)) {
            this.checkTemplateExpression(filePath, node);
            return;
        }

        // 检查二元加法表达式
        if (ts.isBinaryExpression(node) &&
            (node.operatorToken.kind === ts.SyntaxKind.PlusToken ||
                node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken)) {
            this.checkBinaryAddExpression(filePath, node);
            return;
        }
    }

    private traverseChildNodes(filePath: string, node: ts.Node): void {
        ts.forEachChild(node, (childNode) => {
            this.traverseNodes(filePath, childNode);
        });
    }

    private checkVariableDeclaration(filePath: string, node: ts.VariableDeclaration): void {
        if (!node.initializer || !ts.isIdentifier(node.name)) {
            return;
        }

        // 检查三元表达式初始化的变量
        if (ts.isConditionalExpression(node.initializer)) {
            this.checkConditionalExpression(node.name.text, node.initializer);
        }

        // 检查包含 Math.random() 的表达式初始化的变量
        else if (ts.isBinaryExpression(node.initializer) &&
            this.containsMathRandom(node.initializer)) {
            this.trackVariableWithRandomInitializer(node.name.text, node.initializer);
        }
    }

    private checkAssignmentExpression(filePath: string, node: ts.BinaryExpression): void {
        if (!ts.isIdentifier(node.left)) {
            return;
        }

        // 检查三元表达式赋值
        if (ts.isConditionalExpression(node.right)) {
            this.checkConditionalExpression(node.left.text, node.right);
        }

        // 检查包含 Math.random() 的表达式赋值
        else if (ts.isBinaryExpression(node.right) &&
            this.containsMathRandom(node.right)) {
            this.trackVariableWithRandomInitializer(node.left.text, node.right);
        }
    }

    private checkToStringCall(filePath: string, node: ts.CallExpression): void {
        const callText = node.getText().replace(/\s+/g, '');
        if (this.LITERAL_TO_STRING_REGEX.test(callText)) {
            this.reportDefect(
                filePath,
                node,
                Usefulness.Never
            );
            return;
        }

        if (!ts.isPropertyAccessExpression(node.expression)) {
            return;
        }

        const expression = node.expression.expression;
        // 检查对象字面量直接调用 toString
        if (ts.isObjectLiteralExpression(expression) && expression.properties.length === 0) {
            this.reportDefect(filePath, expression, Usefulness.Never);
            return;
        }

        // 检查标识符，如果是已知的危险变量
        if (ts.isIdentifier(expression)) {
            // 检查变量是否在安全转换的条件块中
            if (this.isWithinSafeConversionBlock(expression)) {
                return;
            }

            // 处理确定是对象的变量
            if (this.dangerousVariables.has(expression.text)) {
                this.reportDefect(filePath, expression, Usefulness.Never);
                return;
            }

            // 处理可能是对象的变量
            const varInfo = this.maybeObjectVariables.get(expression.text);
            if (varInfo) {
                // 如果变量已安全转换，则不报告
                if (varInfo.isSafelyConverted || this.safeVariables.has(expression.text)) {
                    return;
                }

                if (varInfo.isDefinitelyObject) {
                    this.reportDefect(filePath, expression, Usefulness.Never);
                } else if (varInfo.isMaybeObject) {
                    this.reportDefect(filePath, expression, Usefulness.Sometimes);
                }
                return;
            }
        }
    }

    private checkTemplateExpression(filePath: string, node: ts.TemplateExpression): void {
        node.templateSpans.forEach(span => {
            this.checkTemplateSpan(filePath, span);
        });
    }

    private checkTemplateSpan(filePath: string, span: ts.TemplateSpan): void {
        const expression = span.expression;

        // 检查空对象字面量在模板中的使用
        if (ts.isObjectLiteralExpression(expression) && expression.properties.length === 0) {
            this.reportDefect(filePath, expression, Usefulness.Never);
            return;
        }

        // 检查标识符是否为危险变量
        if (ts.isIdentifier(expression)) {
            this.checkIdentifierInTemplate(filePath, expression);
        }
    }

    private checkIdentifierInTemplate(filePath: string, expression: ts.Identifier): void {
        // 检查变量是否在安全转换的条件块中
        if (this.isWithinSafeConversionBlock(expression)) {
            return;
        }

        // 检查是否为危险变量
        if (this.dangerousVariables.has(expression.text)) {
            this.reportDefect(filePath, expression, Usefulness.Never);
            return;
        }

        this.checkMaybeObjectVariable(filePath, expression);
    }

    private checkMaybeObjectVariable(filePath: string, expression: ts.Identifier): void {
        const varInfo = this.maybeObjectVariables.get(expression.text);
        if (!varInfo) {
            return;
        }

        // 如果变量已安全转换，则不报告
        if (varInfo.isSafelyConverted || this.safeVariables.has(expression.text)) {
            return;
        }

        if (varInfo.isDefinitelyObject) {
            this.reportDefect(filePath, expression, Usefulness.Never);
        } else if (varInfo.isMaybeObject) {
            this.reportDefect(filePath, expression, Usefulness.Sometimes);
        }
    }

    private checkBinaryAddExpression(filePath: string, node: ts.BinaryExpression): void {
        // 检查左操作数
        this.checkBinaryExpressionOperand(filePath, node.left);

        // 检查右操作数
        this.checkBinaryExpressionOperand(filePath, node.right);
    }

    private checkBinaryExpressionOperand(filePath: string, operand: ts.Expression): void {
        if (ts.isObjectLiteralExpression(operand) && operand.properties.length === 0) {
            this.reportDefect(filePath, operand, Usefulness.Never);
        }
        else if (ts.isIdentifier(operand)) {
            // 检查变量是否在安全转换的条件块中
            if (this.isWithinSafeConversionBlock(operand)) {
                return;
            }

            if (this.dangerousVariables.has(operand.text)) {
                this.reportDefect(filePath, operand, Usefulness.Never);
            }

            // 检查可能是对象的变量
            const varInfo = this.maybeObjectVariables.get(operand.text);
            if (varInfo) {
                // 如果变量已安全转换，则不报告
                if (varInfo.isSafelyConverted || this.safeVariables.has(operand.text)) {
                    return;
                }

                if (varInfo.isDefinitelyObject) {
                    this.reportDefect(filePath, operand, Usefulness.Never);
                } else if (varInfo.isMaybeObject) {
                    this.reportDefect(filePath, operand, Usefulness.Sometimes);
                }
            }
        }
    }

    private markVariableDeclaration(node: ts.VariableDeclaration): void {
        // 如果变量初始化为空对象字面量，标记为危险变量
        if (node.initializer && ts.isObjectLiteralExpression(node.initializer) &&
            node.initializer.properties.length === 0 && ts.isIdentifier(node.name)) {
            this.dangerousVariables.add(node.name.text);
        }

        // 如果变量初始化为 JSON.stringify 或 String 调用，标记为安全变量
        if (node.initializer && ts.isIdentifier(node.name) && this.isSafeStringConversion(node.initializer)) {
            this.safeVariables.add(node.name.text);
            this.dangerousVariables.delete(node.name.text);
        }
    }

    private markAssignmentExpression(node: ts.BinaryExpression): void {
        if (!ts.isIdentifier(node.left)) {
            return;
        }

        if (ts.isObjectLiteralExpression(node.right) && node.right.properties.length === 0) {
            this.dangerousVariables.add(node.left.text);
        }
        else if (this.isSafeStringConversion(node.right)) {
            this.safeVariables.add(node.left.text);
            this.dangerousVariables.delete(node.left.text);

            // 更新变量信息
            const varInfo = this.maybeObjectVariables.get(node.left.text);
            if (varInfo) {
                varInfo.isSafelyConverted = true;
                this.maybeObjectVariables.set(node.left.text, varInfo);
            }
        }
    }

    private checkConditionalExpression(varName: string, expr: ts.ConditionalExpression): void {
        let hasObject = false;
        let hasPrimitive = false;

        // 检查条件表达式的 true 分支
        if (ts.isObjectLiteralExpression(expr.whenTrue)) {
            hasObject = true;
        } else if (ts.isStringLiteral(expr.whenTrue) ||
            ts.isNumericLiteral(expr.whenTrue) ||
            expr.whenTrue.kind === ts.SyntaxKind.TrueKeyword ||
            expr.whenTrue.kind === ts.SyntaxKind.FalseKeyword) {
            hasPrimitive = true;
        }

        // 检查条件表达式的 false 分支
        if (ts.isObjectLiteralExpression(expr.whenFalse)) {
            hasObject = true;
        } else if (ts.isStringLiteral(expr.whenFalse) ||
            ts.isNumericLiteral(expr.whenFalse) ||
            expr.whenFalse.kind === ts.SyntaxKind.TrueKeyword ||
            expr.whenFalse.kind === ts.SyntaxKind.FalseKeyword) {
            hasPrimitive = true;
        }

        // 记录变量信息
        if (hasObject) {
            const variableInfo: VariableInfo = {
                hasObject: hasObject,
                hasPrimitive: hasPrimitive,
                isMaybeObject: hasObject && hasPrimitive,
                isDefinitelyObject: hasObject && !hasPrimitive,
                isSafelyConverted: false
            };

            this.maybeObjectVariables.set(varName, variableInfo);

            // 如果变量肯定是对象，也添加到危险变量列表中
            if (variableInfo.isDefinitelyObject) {
                this.dangerousVariables.add(varName);
            }
        }
    }

    private containsMathRandom(node: ts.Node): boolean {
        if (ts.isPropertyAccessExpression(node) &&
            ts.isIdentifier(node.expression) &&
            node.expression.text === 'Math' &&
            ts.isIdentifier(node.name) &&
            node.name.text === 'random') {
            return true;
        }

        let result = false;
        ts.forEachChild(node, child => {
            if (this.containsMathRandom(child)) {
                result = true;
            }
        });

        return result;
    }

    private trackVariableWithRandomInitializer(varName: string, expr: ts.Expression): void {
        // 如果表达式包含 Math.random()，认为变量可能是任何类型
        this.maybeObjectVariables.set(varName, {
            hasObject: true,
            hasPrimitive: true,
            isMaybeObject: true,
            isDefinitelyObject: false,
            isSafelyConverted: false
        });
    }

    private isSafeStringConversion(expr: ts.Node): boolean {
        return ts.isCallExpression(expr) &&
            (expr.expression.getText() === 'JSON.stringify' ||
                expr.expression.getText() === 'String');
    }

    private isWithinSafeConversionBlock(node: ts.Node): boolean {
        // 如果不是标识符，直接返回false
        if (!ts.isIdentifier(node)) {
            return false;
        }

        const varName = node.text;
        // 检查变量是否在条件块变量集合中
        if (!this.isVariableInConditionalBlocks(varName)) {
            return false;
        }

        return this.isNodeInConditionalBlock(node, varName);
    }

    /**
     * 检查变量是否在条件块变量集合中
     */
    private isVariableInConditionalBlocks(varName: string): boolean {
        const posSet = this.conditionalBlockVariables.get(varName);
        return posSet !== undefined && posSet.size > 0;
    }

    /**
     * 检查节点是否在条件块中
     */
    private isNodeInConditionalBlock(node: ts.Node, varName: string): boolean {
        const nodePos = node.getStart();
        const posSet = this.conditionalBlockVariables.get(varName)!;

        // 查找节点的所有父节点，直到找到if语句
        return this.findIfStatementInParentChain(node, nodePos, posSet);
    }

    /**
     * 在父节点链中查找if语句
     */
    private findIfStatementInParentChain(node: ts.Node, nodePos: number, posSet: Set<number>): boolean {
        let parentNode: ts.Node = node;

        while (parentNode.parent) {
            parentNode = parentNode.parent;

            // 如果找到if语句，检查它是否是我们记录的条件块之一
            if (ts.isIfStatement(parentNode)) {
                if (this.isIfStatementInPosSet(parentNode, nodePos, posSet)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 检查if语句是否在位置集合中
     */
    private isIfStatementInPosSet(ifStatement: ts.IfStatement, nodePos: number, posSet: Set<number>): boolean {
        const ifStatementPos = ifStatement.getStart();

        for (const blockPos of posSet) {
            // 如果当前节点在任何一个记录的条件块中，则认为它是安全的
            if (blockPos <= nodePos && ifStatementPos === blockPos) {
                return true;
            }
        }

        return false;
    }

    private reportDefect(filePath: string, node: ts.Node, certainty: Usefulness): void {
        const normalizedPath = filePath.replace(/\\/g, '/');
        const sourceFile = node.getSourceFile();
        const startPos = node.getStart();
        const { line, character: startCol } = ts.getLineAndCharacterOfPosition(sourceFile, startPos);
        let adjustedCol = startCol + 1;
        if (ts.isCallExpression(node) && node.expression.getText().replace(/\s+/g, '') === '({}).toString') {
            adjustedCol += 1;
        }
        const nodeKey = `${path.basename(normalizedPath)}:${line + 1}:${line + 1}:${certainty}`;

        if (this.reportedNodes.has(nodeKey)) {
            return;
        }
        this.reportedNodes.set(nodeKey, certainty);

        const rawText = node.getText().trim();

        let description = `${this.getCleanDisplayText(node)} `;
        description += certainty === Usefulness.Never
            ? "will evaluate to '[object Object]'"
            : "may evaluate to '[object Object]'";
        description += ' when stringified';
        if (ts.isObjectLiteralExpression(node) && node.properties.length === 0) {
            description = `${description}`;
        }

        const originalLine = this.getOriginalLineNumber(normalizedPath, startPos);
        const endCol = startCol + (node.getEnd() - node.getStart());
        const positionKey = `${path.normalize(normalizedPath)}:${originalLine}:${startCol}:${endCol}`;
        if (this.reportedNodes.has(positionKey)) {
            return;
        }
        this.reportedNodes.set(positionKey, certainty);

        const normalizedText = rawText.replace(/\s+/g, '').replace(/\?\./g, '.');
        if (normalizedText === '{}' && certainty !== Usefulness.Never) {
            return;
        }

        const defect = new Defects(originalLine, adjustedCol, endCol, description, this.metaData.severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false)
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }

    private getCleanDisplayText(node: ts.Node): string {
        const rawText = node.getText().trim();

        if (ts.isCallExpression(node) &&
            node.expression.getText().replace(/\s+/g, '') === '({}).toString') {
            return '\'{}\'';
        }

        if (rawText === '{}') {
            return '\'{}\'';
        }

        if (ts.isIdentifier(node)) {
            return `'${rawText}'`;
        }

        return rawText.length > 20
            ? `'${rawText.slice(0, 17)}...'`
            : `'${rawText}'`;
    }

    private getOriginalLineNumber(filePath: string, pos: number): number {
        const normalizedPath = path.normalize(filePath);
        const lines = this.sourceMap.get(normalizedPath);
        if (!lines || pos < 0) {
            return 0;
        }
        let offset = 0;
        if (lines.length > 0 && lines[0].charCodeAt(0) === 0xFEFF) {
            offset = 1;
        }

        let currentPos = offset;
        for (let lineNum = 0; lineNum < lines.length; lineNum++) {
            const lineLength = Math.max(lines[lineNum].length, 0);
            const lineEnd = currentPos + lineLength + 1;

            if (pos < lineEnd || lineNum === lines.length - 1) {
                return lineNum + 1;
            }
            currentPos = lineEnd;
        }
        return lines.length;
    }
}